﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.IO;
using System.Diagnostics;

namespace EightylegsCustomerResults
{

    /// <summary>
    ///  Class CustomerResults.
    ///  This serializes and deserializes the customer results in 80legs
    ///  
    /// A simple way to get the CustomerResults is to use the static function ReadFile().  
    /// It should be called like this:
    ///   Dictionary<String, byte[]> resultsData = CustomerResults.ReadFile ( fileName );
    ///    
    /// The basic deserialize flow is as follows, but you can replace the BufferedInputStream
    /// with any InputStream if you want to read some other way (e.g. read from memory):
    ///    FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read);
    ///    CustomerResults customerResults = new CustomerResults();
    ///    try {
    ///      customerResults.StartRead(fs);
    ///      String url;
    ///    	while ((url = customerResults.ReadNextUrl(fs)) != null) {
    ///        byte[] customerData = customerResults.ReadNextData(fs);
    ///        
    ///        // do something with the url and customerData
    ///      }
    ///    } 
    ///    catch ( Exception e ) {
    ///      // format error
    ///    } finally {
    ///         fs.Close();
    ///         fs = null;
    ///    }
    ///  
    ///  For people interested in deserializing in other languages, the file format this
    ///  creates and reads is:
    ///    <classID><versionID><URL-SIZE><URL><DATA-SIZE><DATA>
    ///      - the last 4 items (<URL-SIZE><URL><DATA-SIZE><DATA>) repeat for each url/data pair
    ///      - <classID>, <versionID>, <URL-SIZE>, and <DATA-SIZE> are encoded 32-bit integers
    ///      - The url is encoded using UTF-8
    /// </summary>
    public class CustomerResults
    {
        private static readonly int classID = 218217067;
        private static readonly int maxVersionID = 1;

        private byte[] useBytes;  // 4-byte convenience array
        private int curVersionID;

        public CustomerResults()
        {
            useBytes = new byte[4];
        }

        public static Dictionary<String, byte[]> ReadFile(String fileName)
        {
            FileStream fs = new FileStream(fileName, FileMode.Open, FileAccess.Read);
            Dictionary<String, byte[]> resultsData = new Dictionary<String, byte[]>();

            try
            {
                CustomerResults customerResults = new CustomerResults();

                // loop through all of the results and add them to the HashMap
                customerResults.StartRead(fs);
                String url;
                while ((url = customerResults.ReadNextUrl(fs)) != null)
                {
                    byte[] customerData = customerResults.ReadNextData(fs);

                    try
                    {
                        resultsData.Add(url, customerData);
                    }
                    catch (System.ArgumentException ex)
                    {
                    }

                }
            }
            finally
            {
                fs.Close();
                fs = null;
            }

            return resultsData;
        }

        public static Dictionary<String, byte[]> ReadString(String inData)
        {
            byte[] bytes = System.Text.UTF8Encoding.UTF8.GetBytes(inData);
            MemoryStream s = new MemoryStream(bytes);
            CustomerResults customerResults = new CustomerResults();
            Dictionary<String, byte[]> resultsData = new Dictionary<String, byte[]>();

            // loop through all of the results and add them to the HashMap
            customerResults.StartRead(s);
            String url;
            while ((url = customerResults.ReadNextUrl(s)) != null)
            {
                byte[] customerData = customerResults.ReadNextData(s);

                resultsData.Add(url, customerData);
            }

            return resultsData;
        }

        public void StartRead(Stream r)
        {
            // read and validate the classID - this is used to partially validate that this is the right file type.
            r.Read(useBytes, 0, useBytes.Length);
            int curClassID = ByteArrayToInt(useBytes, 0);
            if (curClassID != classID)
            {
                throw (new Exception("Bad classID=" + curClassID + ", should be " + classID +
                        ". This is probably not a valid CustomerResults file."));
            }

            // read and validate the versionID - this is used internally to handle different versions of this file
            r.Read(useBytes, 0, useBytes.Length);
            curVersionID = ByteArrayToInt(useBytes, 0);
            if (curVersionID > maxVersionID)
            {
                throw (new Exception("Bad Version Code=" + curVersionID + ", maxVersionID=" + maxVersionID +
                        ". This is either not a valid file or a newer version of CustomerResults.java is available on the 80legs website"));
            }
        }

        public void StartWrite(Stream w)
        {
            IntToByteArray(classID, useBytes, 0);
            w.Write(useBytes, 0, useBytes.Length);

            curVersionID = maxVersionID;
            IntToByteArray(curVersionID, useBytes, 0);
            w.Write(useBytes, 0, useBytes.Length);
        }


        public int WriteResult(Stream w, String url, byte[] customData)
        {

            int totalBytes = 0;
            totalBytes += WriteBytesAndSize(w, System.Text.UTF8Encoding.UTF8.GetBytes(url));
            totalBytes += WriteBytesAndSize(w, customData);

            return totalBytes;
        }


        /// <summary>
        ///  This method reads a single url from a CustomerResults file 
        /// </summary>
        /// <param name="r">This is a InputStream for the input of the data</param>
        /// <returns> the URL</returns>
        public String ReadNextUrl(Stream r)
        {
            if (curVersionID == 1)
            {
                byte[] urlBytes = ReadBytesAndSize(r);
                if (urlBytes == null)
                {
                    return null;
                }
                return System.Text.UTF8Encoding.UTF8.GetString(urlBytes);
            }
            else
            {
                throw (new Exception("Unknown versionID=" + curVersionID));
            }
        }

        private static void ReadFully(Stream r, ref byte[] buffer, int bytesReadSoFar)
        {
            if (bytesReadSoFar < 0)
            {
                throw new Exception("Illegal argument");
            }
            if (bytesReadSoFar > buffer.Length)
            {
                throw new Exception("Invalid bytesReadSoFar: " + bytesReadSoFar + " buffer.length: " + buffer.Length);
            }
            while (bytesReadSoFar < buffer.Length)
            {
                int len = buffer.Length - bytesReadSoFar;
                int justRead = r.Read(buffer, bytesReadSoFar, len);
                //System.err.println("justRead: "+justRead+" len: "+len+" bytesReadSoFar: "+bytesReadSoFar);
                if (justRead <= 0)
                {
                    throw new IOException("Unexpected EOF");
                }
                bytesReadSoFar += justRead;
            }

            Debug.Assert(bytesReadSoFar == buffer.Length, "Does not read enough to fill buffer.");
        }



        /// <summary>
        /// This method reads a single byte[] from a CustomerResults file 
        /// </summary>
        /// <param name="r">This is a InputStream for the input of the data</param>
        /// <returns>the customer data as a byte[]</returns>
        public byte[] ReadNextData(Stream r)
        {
            if (curVersionID == 1)
            {
                return ReadBytesAndSize(r);
            }
            else
            {
                throw (new Exception("Unknown versionID=" + curVersionID));
            }
        }


        /// <summary>
        /// reads the size as a 32-bit integer then reads and returns that number of bytes
        /// </summary>
        /// <param name="r"></param>
        /// <returns></returns>
        private byte[] ReadBytesAndSize(Stream r)
        {
            int read = r.Read(useBytes, 0, useBytes.Length);
            if (read <= 0)
            {
                // EOF
                return null;
            }
            // ensure n == 4
            ReadFully(r, ref useBytes, read);
            int size = ByteArrayToInt(useBytes, 0);
            if (size < 0)
            {
                throw (new Exception("Bad Size"));
            }
            //System.err.println("size: "+size);
            byte[] b = new byte[size];
            // ensure we read all size bytes
            ReadFully(r, ref b, 0);
            return b;
        }

        /// <summary>
        /// writes the size as a 32-bit integer then writes the bytes
        /// </summary>
        /// <param name="w"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        private int WriteBytesAndSize(Stream w, byte[] b)
        {
            IntToByteArray(b.Length, useBytes, 0);
            w.Write(useBytes, 0, (int)useBytes.Length);
            w.Write(b, 0, (int)b.Length);

            return useBytes.Length + b.Length;
        }

        /// <summary>
        /// convert an int to a byte array in little endian
        /// </summary>
        /// <param name="i"></param>
        /// <param name="b"></param>
        /// <param name="byteStart"></param>
        private static void IntToByteArray(int i, byte[] b, int byteStart)
        {
            b[byteStart + 3] = (byte)((i >> 24) & 0xFF);
            b[byteStart + 2] = (byte)((i >> 16) & 0xFF);
            b[byteStart + 1] = (byte)((i >> 8) & 0xFF);
            b[byteStart + 0] = (byte)(i & 0xFF);
        }

        /// <summary>
        ///  Convert an byte array to an int (assumes byte-array was little endian)
        /// </summary>
        /// <param name="b"></param>
        /// <param name="byteStart"></param>
        /// <returns></returns>
        private static int ByteArrayToInt(byte[] b, int byteStart)
        {
            return ((int)b[byteStart + 3] << 24)
                    | (((int)b[byteStart + 2] & 0xFF) << 16)
                    | (((int)b[byteStart + 1] & 0xFF) << 8)
                    | ((int)b[byteStart + 0] & 0xFF);
        }

    }
}
